All files / src/components/admin EpgManagement.tsx

0% Statements 0/83
0% Branches 0/42
0% Functions 0/22
0% Lines 0/61

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168                                                                                                                                                                                                                                                                                                                                               
'use client';
 
import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import useLoadNamespace from '@/hooks/useLoadNamespace';
import Image from 'next/image';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { epgService, EpgSource, CreateEpgSourceRequest, UpdateEpgSourceRequest, EpgChannelInfo } from '@/services/epg';
 
export default function EpgManagement() {
  useLoadNamespace('admin/epg');
  const [sources, setSources] = useState<EpgSource[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [form, setForm] = useState<CreateEpgSourceRequest>({ name: '', url: '', timezone: 'UTC', cache_ttl_minutes: 15, is_active: true });
  const [selectedSource, setSelectedSource] = useState<EpgSource | null>(null);
  const [channels, setChannels] = useState<EpgChannelInfo[] | null>(null);
  const [loadingChannels, setLoadingChannels] = useState(false);
  const { t } = useTranslation(['admin/epg', 'admin', 'translation']);
 
  const loadSources = async () => {
    setLoading(true); setError(null);
    const res = await epgService.getSources();
    if (res.success) setSources(res.data);
    else setError(res.error?.details || t('errors.loadingSources'));
    setLoading(false);
  };
 
  useEffect(() => { loadSources(); }, []);
 
  const handleCreate = async () => {
    if (!form.name.trim() || !form.url.trim()) { setError(t('errors.nameUrlRequired')); return; }
    const res = await epgService.createSource(form);
    if (res.success) { setForm({ name: '', url: '', timezone: 'UTC', cache_ttl_minutes: 15, is_active: true }); loadSources(); }
    else setError(res.error?.details || t('errors.createFailed'));
  };
 
  const handleUpdate = async () => {
    if (!selectedSource) return;
    const payload: UpdateEpgSourceRequest = { ...form };
    const res = await epgService.updateSource(selectedSource.id, payload);
    if (res.success) { setSelectedSource(null); setForm({ name: '', url: '', timezone: 'UTC', cache_ttl_minutes: 15, is_active: true }); loadSources(); }
    else setError(res.error?.details || t('errors.updateFailed'));
  };
 
  const handleEdit = (src: EpgSource) => {
    setSelectedSource(src);
    setForm({ name: src.name, url: src.url, timezone: src.timezone, cache_ttl_minutes: src.cache_ttl_minutes, is_active: src.is_active });
  };
 
  const handleDelete = async (src: EpgSource) => {
    if (!confirm(t('confirmDelete', { name: src.name }))) return;
    const res = await epgService.deleteSource(src.id);
    if (res.success) loadSources(); else setError(res.error?.details || t('errors.deleteFailed'));
  };
 
  const handleDiscover = async (src: EpgSource) => {
    setLoadingChannels(true); setChannels(null); setError(null);
    const res = await epgService.discoverChannels(src.id);
    if (res.success) setChannels(res.data);
    else setError(res.error?.details || t('errors.fetchChannelsFailed'));
    setLoadingChannels(false);
  };
 
  return (
    <div className="space-y-6">
      <div className="flex items-center justify-between">
        <h2 className="text-2xl font-bold">{t('epg.title')}</h2>
        {error && <div className="text-red-600 text-sm">{error}</div>}
      </div>
 
      <Card>
        <CardHeader>
          <CardTitle>{selectedSource ? t('epg.editSource') : t('epg.createSource')}</CardTitle>
        </CardHeader>
        <CardContent>
          <div className="grid grid-cols-6 gap-4 items-end">
            <div className="col-span-2">
              <Label htmlFor="name">{t('common.name')}</Label>
              <Input id="name" value={form.name} onChange={e => setForm(f => ({ ...f, name: e.target.value }))} placeholder={t('epg.inputPlaceholder')} />
            </div>
            <div className="col-span-3">
              <Label htmlFor="url">{t('epg.xmltvUrl')}</Label>
              <Input id="url" value={form.url} onChange={e => setForm(f => ({ ...f, url: e.target.value }))} placeholder={t('epg.urlPlaceholder')} />
            </div>
            <div>
              <Label htmlFor="ttl">{t('epg.ttl')}</Label>
              <Input id="ttl" type="number" value={form.cache_ttl_minutes ?? 15} onChange={e => setForm(f => ({ ...f, cache_ttl_minutes: parseInt(e.target.value || '15', 10) }))} />
            </div>
            <div className="col-span-1 flex items-center space-x-2">
              <Switch checked={!!form.is_active} onCheckedChange={(v) => setForm(f => ({ ...f, is_active: v }))} />
              <Label>{t('common.active')}</Label>
            </div>
          </div>
          <div className="mt-4 flex gap-2">
            {selectedSource ? (
              <>
                <Button onClick={handleUpdate}>{t('common.save')}</Button>
                <Button variant="outline" onClick={() => { setSelectedSource(null); setForm({ name: '', url: '', timezone: 'UTC', cache_ttl_minutes: 15, is_active: true }); }}>{t('common.cancel')}</Button>
              </>
            ) : (
              <Button onClick={handleCreate}>{t('common.create')}</Button>
            )}
          </div>
        </CardContent>
      </Card>
 
      <Card>
        <CardHeader>
          <CardTitle>{t('epg.sources')}</CardTitle>
        </CardHeader>
        <CardContent>
          {loading ? (
            <div>{t('common.loading')}</div>
          ) : (
            <div className="space-y-2">
              {sources.length === 0 && <div className="text-sm text-gray-500">{t('epg.noSourcesYet')}</div>}
              {sources.map((src) => (
                <div key={src.id} className="flex items-center justify-between border p-3 rounded-md">
                  <div className="space-y-1">
                    <div className="font-medium">{src.name} {src.is_active ? '' : <span className="text-xs text-gray-500">{t('inactive')}</span>}</div>
                    <div className="text-xs text-gray-600 break-all">{src.url}</div>
                    <div className="text-xs text-gray-600">{t('epg.ttl')}: {src.cache_ttl_minutes} {t('units.minutes')} ยท {t('labels.timezone')} {src.timezone}</div>
                  </div>
                  <div className="flex items-center gap-2">
                    <Button variant="outline" onClick={() => handleDiscover(src)}>{t('epg.fetchChannels')}</Button>
                    <Button variant="outline" onClick={() => handleEdit(src)}>{t('common.edit')}</Button>
                    <Button variant="destructive" onClick={() => handleDelete(src)}>{t('common.delete')}</Button>
                  </div>
                </div>
              ))}
            </div>
          )}
        </CardContent>
      </Card>
 
      {channels && (
        <Card>
          <CardHeader>
            <CardTitle>{t('epg.discoveredChannels')} ({channels.length})</CardTitle>
          </CardHeader>
          <CardContent>
            {loadingChannels ? (
              <div>{t('epg.loadingChannels')}</div>
            ) : (
              <div className="grid grid-cols-1 md:grid-cols-2 gap-2 max-h-72 overflow-y-auto">
                {channels.map((ch) => (
                  <div key={ch.id} className="border rounded p-2 flex min-w-0 items-center justify-between gap-3">
                    <div className="min-w-0">
                      <div className="font-medium truncate" title={ch.display_name}>{ch.display_name}</div>
                      <div className="text-xs text-gray-600 truncate" title={ch.id}>{ch.id}</div>
                    </div>
                    {ch.icon && <Image src={ch.icon} alt="" width={32} height={32} className="w-8 h-8 shrink-0 object-contain" />}
                  </div>
                ))}
              </div>
            )}
          </CardContent>
        </Card>
      )}
    </div>
  );
}